/*******************************************************************************
 * Copyright (C) 2014 The Calrissian Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package org.calrissian.restdoclet.util;

import static java.util.Collections.emptyList;
import static org.calrissian.restdoclet.util.CommonUtils.isEmpty;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.calrissian.restdoclet.model.Model;
import org.calrissian.restdoclet.model.ModelProperty;
import org.calrissian.restdoclet.model.ModelRef;
import org.calrissian.restdoclet.model.QueryParam;
import org.calrissian.restdoclet.model.type.ParamTypeInfo;
import org.calrissian.restdoclet.model.type.ParamTypeInfos;
import org.calrissian.restdoclet.model.type.property.ArrayModelPropertyGentor;
import org.calrissian.restdoclet.model.type.property.ConcreteModelPropertyGentor;
import org.calrissian.restdoclet.model.type.property.ConcreteParamerterizedModelPropertyGentor;
import org.calrissian.restdoclet.model.type.property.ModelPropertyGenerator;
import org.calrissian.restdoclet.model.type.property.TypeVairableModelPropertyGentor;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.ParameterizedType;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Type;
import com.sun.javadoc.TypeVariable;

public class TypeUtils {

    /**
     * Will return a full data type for Swagger.
     * 
     * @param type
     * @return
     */
    public static String dataType(Type type) {
        if (type == null)
            return null;

        if (isContainer(type)) {
            // treat sets as sets
            // if (isType(type.asClassDoc(), Set.class))
            // return "Set[" + internalContainerType(type) + "]";

            // return "array[" + internalContainerType(type) + "]";
            return "array";
        }

        // Treat as a basic type.
        return getTypeName(type);
    }

    /**
     * Checks if the type is an iterable or an array
     * 
     * @param type
     * @return
     */
    public static boolean isContainer(Type type) {

        // first check for arrays
        if (type.dimension() != null && !type.dimension().isEmpty())
            return true;

        // treat iterables as lists
        if (isType(type.asClassDoc(), Iterable.class))
            return true;

        return false;
    }

    /**
     * This will grab the internal type from an array or a parameterized container.
     * 
     * @param type
     * @return
     */
    public static String internalContainerType(Type type) {
        // treat arrays first
        if (type.dimension() != null && !type.dimension().isEmpty())
            return getTypeName(type);

        ParameterizedType pType = type.asParameterizedType();
        if (pType != null) {
            Type[] paramTypes = ((ParameterizedType) type).typeArguments();
            if (!isEmpty(paramTypes))
                return getTypeName(paramTypes[0]);
        }

        return type.simpleTypeName();
    }

    /**
     * Returns the basic type. If not one of the supported swagger basic types then it is treated as an Object.
     * 
     * @param type
     * @return
     */
    public static String getTypeName(Type type) {
        if (type == null)
            return "void";

        // next primitives
        if (type.isPrimitive())
            return type.qualifiedTypeName();

        String name = type.qualifiedTypeName();

        // Check the java.lang classes
        if (name.equals(String.class.getName()))
            return "string";

        if (name.equals(Boolean.class.getName()))
            return "boolean";

        if (name.equals(Integer.class.getName()))
            return "int";

        if (name.equals(Long.class.getName()))
            return "long";

        if (name.equals(Float.class.getName()))
            return "float";

        if (name.equals(Double.class.getName()))
            return "double";

        if (name.equals(Byte.class.getName()))
            return "byte";

        if (name.equals(Date.class.getName()))
            return "Date";

        if (name.equals(Void.class.getName()))
            return "void";

        if (name.equals("org.springframework.web.multipart.MultipartFile")) {
            return "file";
        }

        // Process enums as strings.
        if (!isEmpty(type.asClassDoc().enumConstants()))
            return "string";

        ParameterizedType parameterizedType = type.asParameterizedType();
        if (null != parameterizedType) {
            StringBuilder parameterzedName = new StringBuilder("&lt;");
            for (Type typeArgument : parameterizedType.typeArguments()) {
                if (parameterzedName.length() != 4) {
                    parameterzedName.append(", ");
                }
                parameterzedName.append(getTypeName(typeArgument));
            }
            parameterzedName.append("&gt;");
            return name + parameterzedName.toString();
        }
        return name;

    }

    /**
     * This will retrieve all known allowable values from an enum.
     * 
     * @param type
     * @return
     */
    public static Collection<String> allowableValues(Type type) {
        if (type == null || type.asClassDoc() == null)
            return emptyList();

        FieldDoc[] fields = type.asClassDoc().enumConstants();
        if (isEmpty(fields))
            return emptyList();

        Collection<String> values = new ArrayList<String>(fields.length);
        for (FieldDoc field : fields)
            values.add(field.name());

        return values;
    }

    /**
     * Checks the class doc to see if it is a type or subtype of the provided class or object.
     * 
     * @param classDoc
     * @param targetClazz
     * @param <T>
     * @return
     */
    private static <T> boolean isType(ClassDoc classDoc, Class<T> targetClazz) {
        if (classDoc == null)
            return false;

        if (classDoc.qualifiedTypeName().equals(targetClazz.getName()))
            return true;

        if (isType(classDoc.superclass(), targetClazz))
            return true;

        for (ClassDoc iface : classDoc.interfaces())
            if (isType(iface, targetClazz))
                return true;

        return false;
    }

    public static ParamTypeInfos getAllParamTypeInfos(Type type, ParamTypeInfos infos) {
        if (type == null) {
            return infos;
        }
        ParameterizedType parameterizedType = type.asParameterizedType();
        if (parameterizedType != null) {
            ParamTypeInfo paramTypeInfo = new ParamTypeInfo();
            paramTypeInfo.setFullTypeName(parameterizedType.toString());
            TypeVariable[] typeVariables = parameterizedType.asClassDoc().typeParameters();
            Type[] typeVariableTypes = parameterizedType.typeArguments();
            if (typeVariableTypes.length != typeVariables.length) {
                return infos;
            }
            for (int i = 0; i < typeVariables.length; i++) {
                paramTypeInfo.addSymbolTypePair(typeVariables[i].qualifiedTypeName(), typeVariableTypes[i]);
                infos.add(paramTypeInfo);
                if (typeVariableTypes[i].asParameterizedType() != null) {
                    getAllParamTypeInfos(typeVariableTypes[i].asParameterizedType(), infos);
                }
            }
        }
        return infos;
    }

    public static Map<String, Model> getModel(Type type, RootDoc rootDoc) {
        Map<String, Model> result = new HashMap<String, Model>();

        ParamTypeInfos paramTypeInfos = new ParamTypeInfos();
        getAllParamTypeInfos(type.asParameterizedType(), paramTypeInfos);

        getModel(result, type, rootDoc, paramTypeInfos);

        return result;
    }

    public static Map<String, Model> getModel(Map<String, Model> result, Type type,
                                              RootDoc rootDoc, ParamTypeInfos paramTypeInfos) {
        System.out.println("Getting model:" + type.toString());
        if (!needLoad(type) || result.containsKey(dataType(type))) {
            return result;
        }
        String typeName = dataType(type);

        if (typeName.startsWith("array")) {
            Type arrayItemtype = type.asParameterizedType().typeArguments()[0];
            String arrayItemtypeName = dataType(arrayItemtype);
            if (needLoad(arrayItemtype) && !result.containsKey(arrayItemtypeName)) {
                result.putAll(getModel(result, arrayItemtype, rootDoc, getAllParamTypeInfos(arrayItemtype, paramTypeInfos)));
            }
        } else {
            Model model = new Model();
            model.setId(typeName);
            model.setName(typeName);
            if (!isEmpty(type.asClassDoc().enumConstants())) {
                model.setQualifiedType("java.lang.String");
            } else {
                model.setQualifiedType(type.qualifiedTypeName());
            }
            result.put(typeName, model);

            ClassDoc classDoc = rootDoc.classNamed(type.qualifiedTypeName());
            // 类定义不在源码中
            if (null == classDoc) {
                return result;
            }

            // get all fields , includ from super class
            List<FieldDoc> fieldDocList = new LinkedList<>();
            ClassDoc inheritedClassDoc = classDoc;
            do {
                fieldDocList.addAll(Arrays.asList(inheritedClassDoc.fields(false)));
                inheritedClassDoc = inheritedClassDoc.superclass();
            } while (null != inheritedClassDoc);

            if (fieldDocList.size() > 0) {
                for (FieldDoc fieldDoc : fieldDocList) {
                    if (fieldDoc.isStatic()) {
                        continue;
                    }

                    Type fieldType = fieldDoc.type();
                    Type cascadeLoadType = fieldType;

                    // ModelProperty modelProperty = new ModelProperty();
                    // modelProperty.setDescription(fieldDoc.commentText());
                    // modelProperty.setType(dataType(fieldType));

                    TypeVariable typeVariable = fieldType.asTypeVariable();

                    ModelPropertyGenerator[] modelPropertyGenerators = new ModelPropertyGenerator[] {
                                                                                                      new TypeVairableModelPropertyGentor(),
                                                                                                      new ArrayModelPropertyGentor(),
                                                                                                      new ConcreteModelPropertyGentor(),
                                                                                                      new ConcreteParamerterizedModelPropertyGentor()
                    };
                    for (ModelPropertyGenerator modelPropertyGenerator : modelPropertyGenerators) {
                        if (modelPropertyGenerator.belongToThisGenerator(fieldType, type)) {
                            ModelProperty modelProperty = modelPropertyGenerator.generate(fieldType, fieldDoc, type, result, rootDoc, paramTypeInfos);
                            model.getProperties().put(fieldDoc.name(), modelProperty);
                            break;
                        }
                    }

                }
            }
        }
        // }

        return result;
    }

    public static void getQueryParam(Collection<QueryParam> result, Type type,
                                     RootDoc rootDoc, String path) {

        String typeName = dataType(type);

        if (!typeName.startsWith("array")) {
            ClassDoc classDoc = rootDoc.classNamed(type.qualifiedTypeName());
            // 类定义不在源码中
            if (null == classDoc) {
                return;
            }

            // get all fields , includ from super class
            List<FieldDoc> fieldDocList = new LinkedList<>();
            ClassDoc inheritedClassDoc = classDoc;
            do {
                fieldDocList.addAll(Arrays.asList(inheritedClassDoc.fields(false)));
                inheritedClassDoc = inheritedClassDoc.superclass();
            } while (null != inheritedClassDoc);

            if (!CommonUtils.isEmpty(fieldDocList)) {
                for (FieldDoc fieldDoc : fieldDocList) {
                    if (fieldDoc.isStatic()) {
                        continue;
                    }
                    Type fieldType = fieldDoc.type();
                    ModelProperty modelProperty = new ModelProperty();
                    modelProperty.setDescription(fieldDoc.commentText());
                    modelProperty.setType(dataType(fieldType));
                    Type cascadeLoadType = fieldType;
                    if (dataType(fieldType).startsWith("array")) {
                        Type fieldItemType = fieldType.asParameterizedType().typeArguments()[0];
                        ModelRef fieldModelRef = new ModelRef();
                        fieldModelRef.setType(getTypeName(fieldItemType));
                        modelProperty.setItems(fieldModelRef);
                        cascadeLoadType = fieldItemType;
                    } else {
                        if (null != fieldType.asClassDoc() && !isEmpty(fieldType.asClassDoc().enumConstants())) {
                            modelProperty.setQualifiedType("java.lang.String");
                        } else {
                            modelProperty.setQualifiedType(fieldType.qualifiedTypeName());
                        }
                    }
                    // 级联加载
                    String curPath = "".equals(path) ? fieldDoc.name() : path + "." + fieldDoc.name();
                    if (needLoad(cascadeLoadType)) {
                        getQueryParam(result, cascadeLoadType, rootDoc, curPath);
                    } else {
                        result.add(new QueryParam(curPath, false, fieldDoc.commentText(), fieldType));
                    }

                }
            }
        }

    }

    public static boolean needLoad(Type type) {
        return !type.isPrimitive() && !type.qualifiedTypeName().startsWith("java.lang")
               && isEmpty(type.asClassDoc().enumConstants())
               && !type.qualifiedTypeName().startsWith("java.util.Date")
               && !type.qualifiedTypeName()
                   .startsWith("org.springframework.web.multipart.MultipartFile");
    }

}
